/*
 * $Id: LineGraph.java 40 2013-01-08 04:03:14Z rob@patientsknowbest.com $ 
 */
package com.representqueens.spark;

/*
 * 
 * Copyright 2006 Larry Ogrodnek
 * Copyright 2011 Rob Whelan for Patients Know Best
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

import com.representqueens.spark.util.GraphInfo;

/**
 * Linegraph generation.
 * 
 * @author Larry Ogrodnek <larry@cheesesteak.net>
 * @author Rob Whelan <rob@patientsknowbest.com>
 * @version $Revision: 40 $ $Date: 2013-01-08 04:03:14 +0000 (Tue, 08 Jan 2013) $
 */
public class LineGraph {
	private static final int WIDTH = 100;
	private static final int HEIGHT = 25;

	public static final SizeParams DEFAULT_SIZE = new SizeParams(WIDTH, HEIGHT, 0); // spacing not used

	public static final Color DEFAULT_COLOR = Color.GRAY;
	
	public static final Color DEFAULT_RANGE_COLOR = Color.LIGHT_GRAY;

	/**
	 * Create a Linegraph using defaults.
	 * 
	 * @param data
	 *            Array of Number Objects to graph.
	 * 
	 * @return BufferedImage containing a Linegraph of data.
	 */
	public static BufferedImage createGraph(final List<DataPoint> data) {
		return createGraph(data, DEFAULT_SIZE, DEFAULT_COLOR);
	}

	/**
	 * Create a Linegraph.
	 * 
	 * @param data
	 *            Array of Number Objects to graph.
	 * @param size
	 *            SizeParams specifying graph size attributes.
	 * @param color
	 *            graph Color.
	 * 
	 * @return BufferedImage containing a Linegraph of data.
	 */
	public static BufferedImage createGraph(final List<DataPoint> data,
			final SizeParams size, final Color color) {
		return createGraph(data, size, color, null, null);
	}

	/**
	 * Create a Linegraph.
	 * 
	 * @param data
	 *            Array of Number Objects to graph.
	 * @param size
	 *            SizeParams specifying graph size attributes.
	 * @param color
	 *            graph Color.
	 * @param background
	 *            background color, or null for transparent.
	 * @param rangeColor
	 *            background range color.
	 * 
	 * @return BufferedImage containing a Linegraph of data.
	 */
	public static BufferedImage createGraph(final List<DataPoint> data,
			final SizeParams size, final Color color, final Color background, final Color rangeColor) {
		final BufferedImage bi = new BufferedImage(size.getImgWidth(),
				size.getImgHeight(), BufferedImage.TYPE_INT_ARGB);

		if (data == null || data.size() < 1) {
			return bi;
		}

		final Graphics2D g = bi.createGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
				RenderingHints.VALUE_ANTIALIAS_ON);

		if (background != null) {
			g.setBackground(background);
			g.clearRect(0, 0, size.getImgWidth(), size.getImgHeight());
		}

		GraphInfo info = GraphUtils.getGraphInfo(data, size);

		if (info.rangesFound) {
			// loop through once and build range shape; render that before the main graph line
			// some parts of the range may be missing! SO: render the whole thing (based on data
			// points available), then overwrite missing data points with bg-color blocks (that's
			// much simpler than trying to render single data points, etc.)
			boolean rangesMissing = false;
			Path2D.Double rangePath = new Path2D.Double(); // start with rangeHigh
			Stack<double[]> returnPath = new Stack<double[]>(); // for the return back to start along rangeLow
			
			// if the first datapoint doesn't have a range, fake it -- the missing points will be erased, but
			// we need the shape in place
			if (! data.get(0).hasRange()) {
				DataPoint dpRange1 = data.get(info.firstRangedIndex);
				double yHigh = GraphUtils.getGraphY(dpRange1.getRangeHigh(), info);
				double yLow = GraphUtils.getGraphY(dpRange1.getRangeLow(), info);
				rangePath.moveTo(0d, yHigh);
				returnPath.add(new double[] { 0d, yLow });
			}
			
			for (DataPoint p : data) {
				if (p.hasRange()) {
					double x = GraphUtils.getGraphX(p.getX(), info);
					double yHigh = GraphUtils.getGraphY(p.getRangeHigh(), info);
					double yLow = GraphUtils.getGraphY(p.getRangeLow(), info);

					if (rangePath.getCurrentPoint() == null) {
						// first point in shape
						rangePath.moveTo(x, yHigh);
					} else {
						rangePath.lineTo(x, yHigh);
					}
					returnPath.add(new double[] { x, yLow });
				} else {
					rangesMissing = true;
				}
			}
			
			// single point: show range all the way across
			if( info.singlePoint && data.get(0).hasRange() ) {
				DataPoint p = data.get(0);
				double yHigh = GraphUtils.getGraphY(p.getRangeHigh(), info);
				double yLow = GraphUtils.getGraphY(p.getRangeLow(), info);

				rangePath.lineTo(info.size.getImgWidth(), yHigh);
				returnPath.add(new double[] { info.size.getImgWidth(), yLow });
			}

			// if the last datapoint didn't have a range, fake it -- the missing points will be erased, but
			// we need the shape in place
			if (! info.singlePoint && ! data.get(data.size()-1).hasRange()) {
				DataPoint dpRangeL = data.get(info.firstRangedIndex);
				double yHigh = GraphUtils.getGraphY(dpRangeL.getRangeHigh(), info);
				double yLow = GraphUtils.getGraphY(dpRangeL.getRangeLow(), info);
				rangePath.lineTo(info.size.getImgWidth(), yHigh);
				returnPath.add(new double[] { info.size.getImgWidth(), yLow });
			}
			
			// all done with the top line -- now build in the return path along
			// the bottom of the shape
			while (! returnPath.isEmpty()) {
				double[] p = returnPath.pop();
				rangePath.lineTo(p[0], p[1]);
			}
			rangePath.closePath();

			if (rangesMissing) {
				// convert to Area and snip out the bits where we don't have range data
				Area rangeArea = new Area(rangePath);

				// overwrite the range with bands of bg color where the range
				// values were null
				for (int i = 0; i < data.size(); i++) {
					DataPoint p = data.get(i);
					if (! p.hasRange()) {
						Rectangle2D.Double block = GraphUtils.getRectangleAroundPoint(data, i, info);
						rangeArea.subtract(new Area(block));
					}
				}
				// render the range minus snipped-out bits
				g.setPaint(rangeColor);
				g.fill(rangeArea);
			}
			else {
				// no need to convert to Area
				g.setPaint(rangeColor);
				g.fill(rangePath);
			}
		}

		Path2D.Double dataPath = new Path2D.Double();
		// track point shapes; paint them after rendering the line
		List<Shape> shapes = new ArrayList<Shape>();
		List<Color> shapeColors = new ArrayList<Color>();
		for (DataPoint p : data) {
			double px = GraphUtils.getGraphX(p.getX(), info);
			if( info.singlePoint ) // display to the right instead of defaulting to the left...
				px = info.size.getWidth() + SizeParams.PADDING;
			double py = GraphUtils.getGraphY(p.getY(), info);
			
			if (dataPath.getCurrentPoint() == null) // first point
				dataPath.moveTo(px, py);
			else
				dataPath.lineTo(px, py);
			
			if( p.hasShape() ) {
				shapes.add( new Ellipse2D.Double(px - 2.5, py - 2.5, 5, 5) );
				shapeColors.add( p.getColor()==null ? color : p.getColor() );
			}
		}
		g.setPaint(color);
		g.draw(dataPath);
		
		for (int i=0; i<shapes.size(); i++) {
			g.setPaint(shapeColors.get(i));
			g.fill(shapes.get(i));
		}

		return bi;
	}
}
